/*******************************************************************************
 * Copyright (c) 2009 LegSem.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     LegSem - initial API and implementation
 ******************************************************************************/
package com.legstar.zosjes;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPReply;

/**
 * Manages an FTP connection to a z/OS server.
 * <p/>
 * Makes it simple to upload ASCII files and submit JCL for execution.
 *
 */
public class FtpZosClient {
	
	/** Apache commons net FTP client.*/
	FTPClient _ftpClient;
	
	/** The expected string reply from FTP z/OS on job submission. */
	private static final String SUBMIT_REPLY = "250-It is known to JES as ";
	
	/** Condition code the way it appears in jobs held output.*/
	private static final Pattern COND_CODE_PATTERN =
		Pattern.compile("COND CODE (\\d{4})", Pattern.CASE_INSENSITIVE);

	/**
	 * No-arg constructor.
	 */
	public FtpZosClient() {
		_ftpClient = new FTPClient();
		FTPClientConfig ftpConf = new FTPClientConfig(FTPClientConfig.SYST_MVS);
		ftpConf.setServerTimeZoneId("GMT");
		_ftpClient.configure(ftpConf);
	}
	
	/**
	 * Open an FTP connection to the mainframe. 
	 * @param hostname the mainframe IP address
	 * @param hostUserID the mainframe user ID used to authenticate
	 * @param hostPassword the mainframe password used to authenticate
	 * @throws IOException if connection fails
	 */
	public void open(
			final String hostname,
			final String hostUserID,
			final String hostPassword) throws IOException {
		if (_ftpClient.isConnected()) {
			_ftpClient.disconnect();
		}
		_ftpClient.connect(hostname);
		if (!FTPReply.isPositiveCompletion(_ftpClient.getReplyCode())) {
			throw new IOException(hostname + " not responding");
		}
		if (!_ftpClient.login(hostUserID, hostPassword)) {
			processFtpError();
		}
	}
	
	/**
	 * Upload a single file to the mainframe.
	 * @param remote the z/OS name of the file
	 * @param local the local file
	 * @throws IOException if upload fails
	 */
	public void upload(final String remote, final File local) throws IOException {
		if (!_ftpClient.sendSiteCommand("FILEtype=SEQ")) {
			processFtpError();
		}
		if (!_ftpClient.storeFile(remote, new FileInputStream(local))) {
			processFtpError();
		}
	}
	
	/**
	 * Submits the job passed as a string.
	 * <p/>
     * Upon return the job is queued in JES. It is possible to query
     * the status of the job.
	 * @param jcl a string containing JCL to submit
	 * @return the JES job ID that was assigned
	 * @throws IOException if submit fails
	 */
	public String submitJob(final String jcl) throws IOException {
		
		String jobId = null;
		if (!_ftpClient.sendSiteCommand("FILEtype=JES")) {
			processFtpError();
		}
		
        OutputStream os = _ftpClient.storeFileStream("P390JCL8");
        if (os == null) {
        	processFtpError();
        }
        os.write(jcl.getBytes());
        os.close();
        if (!_ftpClient.completePendingCommand()) {
        	processFtpError();
        }
        
        String[] replies = _ftpClient.getReplyStrings();
        if (replies == null || replies.length == 0) {
        	processFtpError();
        }
        
        if (replies[0].startsWith(SUBMIT_REPLY)) {
        	jobId = replies[0].substring(SUBMIT_REPLY.length());
        } else {
        	processFtpError();
        }
		
		return jobId;
	}
	
	/**
	 * Retrieves the output of a job.
	 * @param jobId the job ID to retrieve
	 * @return the content of the job output files
	 * @throws IOException if something goes wrong
	 */
	public String getJobOutput(final String jobId) throws IOException {
		return getJesResource(jobId + ".x");
	}
	
	/**
	 * Assuming a JCL is available on the mainframe ready for submission,
	 * this will submit that JCL and wait until a result is available.
	 * @param  remoteFile the file on the server that holds the JCL
	 * @return the content of the submitted job output files
	 * @throws IOException if something goes wrong
	 */
	public String submitWaitForOutput(final String remoteFile) throws IOException {
		return getJesResource(remoteFile);
	}

	/**
	 * Generic request to get something back from Jes.
	 * @param  a job id or file name holding JCL to submit
	 * @return the content of the submitted job output files
	 * @throws IOException if something goes wrong
	 */
	public String getJesResource(final String jesResource) throws IOException {
		if (!_ftpClient.sendSiteCommand("FILEtype=JES")) {
			processFtpError();
		}
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		if (!_ftpClient.retrieveFile(jesResource, baos)) {
			processFtpError();
		}
		baos.close();
		String result = baos.toString("UTF-8"); // TODO why UTF-8?
		return result;
	}
	
	/**
	 * Extracts the highest condition code from a job output.
	 * @param heldOutput the job held output
	 * @return the highest condition code
	 */
	public int getHighestCondCode(final String heldOutput) {
		int maxCondCode = -1;
		Matcher matcher = COND_CODE_PATTERN.matcher(heldOutput);
		while(matcher.find()) {
			int condCode = Integer.parseInt(matcher.group(1));
			maxCondCode = (condCode > maxCondCode) ? condCode : maxCondCode;
		}
		return maxCondCode;
	}

	/**
	 * Close an FTP connection to the mainframe.
	 * <p/>
	 * Not a real problem if we don't, the mainframe never keeps a connection
	 * around for very long anyway.
	 * @throws IOException if close fails
	 */
	public void close() throws IOException {
		if (_ftpClient.isConnected()) {
			_ftpClient.logout();
			_ftpClient.disconnect();
		}
	}

	/**
	 * Turns all FTP errors to IO exceptions.
	 * @throws IOException systematic
	 */
	protected void processFtpError() throws IOException {
		String errors[] = _ftpClient.getReplyStrings();
		_ftpClient.disconnect();
		
		if (errors == null || errors.length == 0) {
			throw new IOException("Unknown error.");
		}
		throw new IOException(errors[0]);
	}
}
